Skip to content

[English version](/posts/c教程/en-US/08-Debugging Testing Profiling/078-Crash Analysis and Core Dumps)

即使进行了仔细的编码和测试,程序也会崩溃。在 C 语言中,崩溃是系统表达“你触及了不该触及的东西”的方式。好消息是,您可以使用核心转储(故障时程序内存的快照)科学地分析崩溃。

学习如何阅读它们是每个系统程序员的一项基本技能。

步骤 1.什么是核心转储? ​

核心转储是一个捕获程序崩溃时的状态(堆栈、寄存器、内存)的文件。您可以稍后使用 gdb 之类的调试器检查它,看看出了什么问题。

生成核心转储的常见崩溃信号:

  • SIGSEGV – 无效的内存访问(分段错误)
  • SIGABRT – 断言失败
  • SIGFPE – 算术错误(如被零除)
  • SIGILL – 非法指令

步骤 2. 启用核心转储 ​

默认情况下,现代系统会限制或禁用它们。使用以下方法启用它们:

ulimit -c unlimited

查看:

ulimit -a | grep core

现在,当你的程序崩溃时,一个名为core或者core. 将会出现。

步骤 3. 一个崩溃的例子 ​

让我们做一个简单的崩溃:

#include <stdio.h>
int main(void) {
    int *p = NULL;
    *p = 42;  // crash: dereferencing null pointer
    return 0;
}

编译并运行:

gcc crash.c -g -o crash
./crash

输出:

Segmentation fault (core dumped)

一个core文件现已在您的目录中创建。

步骤 4. 在 gdb 中打开核心转储 ​

使用 gdb 检查发生了什么:

gdb ./crash core

你会看到:

Core was generated by `./crash'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000401136 in main () at crash.c:5
5       *p = 42;

这会告诉您程序崩溃的确切行。

步骤 5. 检查变量和堆栈 ​

在 gdb 中:

(gdb) bt

显示回溯,即崩溃时的调用堆栈。

#0  main () at crash.c:5

您可以检查变量:

(gdb) info locals
(gdb) print p

输出:

$1 = (int *) 0x0

步骤 6. 更复杂的示例 ​

#include <stdio.h>
void f3(int *p) {
    *p = 7;  // likely crash here
}
void f2(int *p) {
    f3(p);
}
void f1(void) {
    int *x = NULL;
    f2(x);
}
int main(void) {
    f1();
}

运行它,使其崩溃,然后:

gdb ./crash core

内部gdb:

(gdb) bt
#0  f3 (p=0x0) at crash.c:4
#1  f2 (p=0x0) at crash.c:8
#2  f1 () at crash.c:12
#3  main () at crash.c:16

您可以看到导致空取消引用的整个调用链。

步骤 7. 调试优化构建 ​

当你编译时-O2或者-O3,编译器可能会内联或重新排序代码,从而使调试变得更加困难。对于调试,请始终使用:

gcc -g -O0 crash.c -o crash

这-gflag 保存符号信息(文件名、行号)。没有它,除了地址之外,gdb 无法告诉您更多信息。

步骤 8. 控制核心转储位置 ​

更改转储的存储位置:

sudo sysctl -w kernel.core_pattern=/tmp/core.%e.%p

这个例子将它们保存在/tmp包含程序名称和进程 ID:

/tmp/core.crash.1234

步骤 9. 崩溃分析工作流程 ​

启用转储:ulimit -c unlimited

运行程序直到崩溃

找到核心文件

用gdb分析:

-bt用于堆栈跟踪 -info locals对于本地变量 -frame 检查每个堆栈帧 -list查看附近的代码

这可以让您全面了解失败之前发生的情况。

步骤 10. 小代码:断言失败分析 ​

#include <assert.h>
#include <stdio.h>
int main(void) {
    int x = 5;
    assert(x == 10);  // fails
    printf("Done\n");
}

编译并运行:

gcc -g assert_fail.c -o assert_fail
ulimit -c unlimited
./assert_fail

输出:

assert_fail: assert_fail.c:5: main: Assertion `x == 10' failed.
Aborted (core dumped)

现在检查:

gdb ./assert_fail core
(gdb) bt

输出:

#0  0x00007f0a9a4b9187 in raise () from /lib64/libc.so.6
#1  0x00007f0a9a4c53e8 in abort () from /lib64/libc.so.6
#2  0x00007f0a9a4b0246 in __assert_fail_base () from /lib64/libc.so.6
#3  0x00007f0a9a4b02f2 in __assert_fail () from /lib64/libc.so.6
#4  0x0000000000401136 in main () at assert_fail.c:5

您可以准确跟踪断言如何导致中止信号。

为什么它很重要 ​

崩溃分析将混乱变得清晰。您可以:

  • 查看哪一行导致了崩溃。
  • 检查此时的所有变量。
  • 快速重现并修复错误。

每个认真的 C 程序员都必须掌握这一点,这是系统工程师调试从用户工具到内核的所有内容的方法。

自己尝试一下 ​

  1. 编写一个取消引用空指针的小程序。
  2. 启用核心转储(ulimit -c unlimited).
  3. 崩溃,然后在 gdb 中分析转储。
  4. 再添加一层函数并检查调用链。
  5. 尝试SIGFPE(除以零)并查看核心转储有何不同。

接下来,您将为 C 项目、习惯和原则构建一个代码审查清单,以帮助防止这些崩溃发生。

本站总访问 — 次